import java.math.BigDecimal;
import java.util.Calendar;
import java.util.LinkedList;

public class ProcessingUnit implements IProcessingUnit
{
	private int processor_id;
	private long processor_memory;
	private int processor_cores;
	private long freeMemory;
	
	private double power_factor;
	private double powerConsumption;
	
	private long firstContinousNanoSecond = 0;
	
	private LinkedList<IServicedJob> running_jobs = null;
	
	protected volatile Boolean running;
	
	private enum System_State {Working, Sleep_Stage1, Sleep_Stage2, Sleep_Stage3};
	private System_State systemState;
	
	public ProcessingUnit(int pID,long pMemory, int pCores, double pF, Boolean running)
	{
		this.processor_id = pID;
		this.processor_memory = pMemory;
		this.processor_cores = pCores;
		this.power_factor = calculateAccurateDouble(pF);
		this.running = running;
		this.freeMemory = processor_memory;
		
		running_jobs = new LinkedList<IServicedJob>();
		
		setCurrentTimeValue();
		startPowerComputationThread();
	}
	
	private double calculateAccurateDouble(double value)
	{
		BigDecimal decimal = new BigDecimal(value);
		decimal = decimal.setScale(2, BigDecimal.ROUND_HALF_UP);
		
		return decimal.doubleValue();
	}
	
	public double getPowerConsumption()
	{
		return calculateAccurateDouble(powerConsumption);
	}
	
	public int getProcessorID()
	{
		return this.processor_id;
	}
	
	public long getFreeMemory()
	{
		return freeMemory;
	}
	
	public long getTotalMemory()
	{
		return processor_memory;
	}
	
	public int getNumberOfProcessingElements()
	{
		return this.processor_cores;
	}
	
	public int getRunningJobCount()
	{
		return running_jobs.size();
	}
	
	public String getSystemState()
	{
		return systemState.toString();
	}
	
	public IServicedJob getRunningJob(int index) throws Exception
	{
		return this.running_jobs.get(index);
	}
	
	public void addJob(IJob newJob)
	{
		// This method is only called when the server object has confirmed that this processing unit has the capability to execute this job
		// Hence, this method should execute the job immediately and do the steps and add it to the serviced job list.
		
		// Also check if the resource was sleeping
		// if yes, wake up and do some stuff as previously !!
		
		System.out.println("New Job added to processor !!");
		
		checkSleepStageAndWakeUpServer();
		setCurrentTimeValue();
		
		double extraConsumption = calculateConsumption(newJob);
		
		freeMemory = freeMemory - newJob.getMemoryRequirement();
		powerConsumption = powerConsumption + extraConsumption;
		
		IServicedJob tempJob = new ServicedJob(newJob,Calendar.getInstance(),extraConsumption);
		running_jobs.add(tempJob);
	}
	
	private double calculateConsumption(IJob newJob)
	{
		// method to calculate the amount of processor power the job would use.
		
		int coresToUse = newJob.getNPE();
		long memoryReq = newJob.getMemoryRequirement();

		// According to the number of cores being used, calculate the extra consumption
		
		double fractionalCoreUsage = calculateAccurateDouble((double) (coresToUse)/ (double) (processor_cores));
		
		return (memorytoMegaBytes(memoryReq) * power_factor * (2 - fractionalCoreUsage));		// Power in Watt-Hours
	}
	
	private void startPowerComputationThread()
	{
		Runnable PowerComputationThread = new Runnable()
		{
			public void run()
			{
				// Method to keep track of jobs which have finished and update the state of the processor.
				
				while(running)
				{
					if(!running_jobs.isEmpty())
					{
						for(int i=0;i<running_jobs.size();i++)
						{
							if(running_jobs.get(i).isJobOver(Calendar.getInstance()))
							{
								powerConsumption = powerConsumption - running_jobs.get(i).getPowerConsumed();
								
								freeMemory = freeMemory + running_jobs.get(i).getJob().getMemoryRequirement();
								
								System.out.println("Job Name: " + running_jobs.get(i).getJob().getJobName() + " removed !!");
								
								running_jobs.remove(i);
							}
						}
						
						//if(running_jobs.isEmpty())
							setCurrentTimeValue();
					}
					
					else
					{
						// Calculate minimum power consumption and accommodate sleep stages.
						
						powerConsumption = calculateMinimumPowerConsumption();
					}
					
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		
		Thread powerThread = new Thread(PowerComputationThread);
		powerThread.start();
	}
	
	private double calculateMinimumPowerConsumption()
	{
		// Stage 0 No Sleep - Until 1st Minute
		// Stage 1 Sleep - After 1 Minute from "first time" - Minutes 1-3
		// Stage 2 Sleep - After 3 Minutes from "first time" - Minute 3-6
		// Stage 3 Sleep - After 6 Minutes from "first time" - Minute 6 & onwards
		
		// check for stages and perform action accordingly
		
		double diff = calculateDifference();
		
		if((diff > 60.0) && (diff <= 180.0))
		{
			systemState = System_State.Sleep_Stage1;
			return ((1.0 * power_factor) * (memoryToGigaBytes(processor_memory)));
		}
		
		else if((diff > 180.0) && (diff <= 360.0))
		{
			systemState = System_State.Sleep_Stage2;
			return ((0.8 * power_factor) * (memoryToGigaBytes(processor_memory)));	
		}
		
		else if(diff > 360.0)
		{
			systemState = System_State.Sleep_Stage3;
			return ((0.5 * power_factor) * (memoryToGigaBytes(processor_memory)));
		}
				
		systemState = System_State.Working;
		return ((1.5 * power_factor) * (memoryToGigaBytes(processor_memory)));
	}
	
	private double calculateDifference()
	{
		long currentNanoSeconds = System.nanoTime();
		long tempDiff2 = currentNanoSeconds - this.firstContinousNanoSecond;
		
		BigDecimal diff = new BigDecimal(tempDiff2/1000000000.0);
		diff = diff.setScale(2,BigDecimal.ROUND_HALF_UP);
		
		//System.out.println("Diff: " + diff.doubleValue());
		
		return diff.doubleValue();
	}
	
	private void setCurrentTimeValue()
	{
		this.firstContinousNanoSecond = System.nanoTime();
	}
	
	private void checkSleepStageAndWakeUpServer()
	{
		if(systemState.equals(System_State.Working))
		{
			// do nothing
		}
		
		else if(systemState.equals(System_State.Sleep_Stage1))
		{
			// calculate base power consumption to wake up from stage 1
			
			powerConsumption = 0.8 * power_factor * (memoryToGigaBytes(processor_memory));
			systemState = System_State.Working;
		}
		
		else if(systemState.equals(System_State.Sleep_Stage2))
		{
			// calculate base power consumption to wake up from stage 2
			
			powerConsumption = 1.0 * power_factor * (memoryToGigaBytes(processor_memory));
			systemState = System_State.Working;
		}
		
		else
		{
			// calculate base power consumption to wake up from stage 3
			
			powerConsumption = 1.2 * power_factor * (memoryToGigaBytes(processor_memory));
			systemState = System_State.Working;
		}
	}
	
	private double memoryToGigaBytes(long givenMemory)
	{
		// Convert the given memory in KiloBytes to GigaBytes
		
		double convertedMemory = 0.0;
		
		convertedMemory = ((double)givenMemory/1024)/1024;
		
		return calculateAccurateDouble(convertedMemory);
	}
	
	private double memorytoMegaBytes(long givenMemory)
	{
		double convertedMemory = 0.0;
		
		convertedMemory = ((double)givenMemory/1024);
		
		return calculateAccurateDouble(convertedMemory);
	}
}
